16.6. 结构体
数组和结构体是 C 支持创建数据元素集合的两种方式。数组用于创建相同类型的数据元素的有序集合,而结构体用于创建 不同类型的 数据元素集合。C 程序员可以以多种不同的方式组合数组和结构体构建块,以创建更复杂的数据类型和结构。本节介绍结构体,在第 2 章中,我们更详细地描述结构体并展示如何将它们与数组组合。
C 不是面向对象语言;因此,它不支持类。但是,它支持定义结构化类型,它们类似于类的公共数据部分。struct
是一种用于表示异构数据集合的类型;它是一种将一组不同类型视为单个、连贯的单元的机制。C 结构在各个数据值之上提供了一个抽象级别,将它们视为单一类型。例如,学生有姓名、年龄、平均绩点 (GPA) 和毕业年份。程序员可以定义一种新的 struct
类型,将这四个数据元素组合成一个 struct student
变量,该变量包含一个姓名值(类型 char []
,用于保存字符串)、一个年龄值(类型 int
)、一个 GPA 值(类型 float
)和一个毕业年份值(类型 int
)。这种结构类型的单个变量可以存储特定学生的所有四部分数据;例如(“Freya”,19,3.7,2021)。
在 C 程序中定义和使用struct
类型有三个步骤:
- 定义一个代表结构的新
struct
类型。 - 声明新的
struct
类型的变量。 - 使用点(
.
)符号来访问变量的各个字段值。
16.6.1. 定义结构体类型
结构体类型定义应出现在 任何函数的外部 ,通常位于程序的 .c 文件的顶部附近。定义新结构体类型的语法如下(struct
是保留关键字):
struct <struct_name> {
<field 1 type> <field 1 name>;
<field 2 type> <field 2 name>;
<field 3 type> <field 3 name>;
...
};
下面是定义一个新的 struct studentT
类型来存储学生数据的示例:
struct studentT {
char name[64];
int age;
float gpa;
int grad_yr;
};
这个结构体定义在 C 的类型系统中增加了一个新类型,类型名称为 struct studentT
。这个结构体定义了四个字段,每个字段定义都包括字段的类型和名称。注意,在这个例子中,name
字段的类型是一个字符数组,用于用作字符串。
16.6.2. 声明结构体类型的变量
定义类型后,即可声明新类型struct studentT
的变量。请注意,与我们迄今为止遇到的仅由一个单词组成的其他类型(例如int
、char
和float
)不同,新结构类型的名称由两个单词组成,struct studentT
。
struct studentT student1, student2; // student1, student2 are struct studentT
16.6.3. 访问字段值
要访问结构变量中的字段值,请使用 点表示法 :
<variable name>.<field name>
访问结构体及其字段时,请仔细考虑所用变量的类型。C 语言新手程序员经常会因为没有考虑到结构体字段的类型而导致程序中出现错误。表 1 显示了围绕struct studentT
类型的几个表达式的类型。
表 1. 与各种 Struct studentT 表达式相关的类型
Expression | C type |
---|---|
student1 | struct studentT |
student1.age | integer (int ) |
student1.name | array of characters (char [] ) |
student1.name[3] | character (char ), the type stored in each position of the name array |
以下是分配struct studentT
变量字段的一些示例:
// The 'name' field is an array of characters, so we can use the 'strcpy'
// string library function to fill in the array with a string value.
strcpy(student1.name, "Kwame Salter");
// The 'age' field is an integer.
student1.age = 18 + 2;
// The 'gpa' field is a float.
student1.gpa = 3.5;
// The 'grad_yr' field is an int
student1.grad_yr = 2020;
student2.grad_yr = student1.grad_yr;
图 1 说明了在上例中字段赋值之后,变量 student1
在内存中的布局。只有结构体变量的字段(方框中的区域)存储在内存中。为了清晰起见,图中标记了字段名称,但对于 C 编译器来说,字段只是存储位置或从结构体变量内存起始处的偏移量。例如,根据 struct studentT
的定义,编译器知道要访问名为 gpa
的字段,它必须跳过一个包含 64 个字符(name
)和一个整数(age
)的数组。请注意,在图中,name
字段仅描述了 64 个字符数组的前六个字符。
图 1. 分配每个字段后 student1 变量的内存
C 结构体类型是左值(lvalues),这意味着它们可以出现在赋值语句的左侧。因此,可以使用简单的赋值语句将一个结构体变量赋给另一个结构体变量的值。赋值语句右侧结构体的字段值被复制到赋值语句左侧结构体的字段值。换句话说,一个结构体的内存内容被复制到另一个结构体的内存中。下面是以这种方式分配结构体值的示例:
student2 = student1; // student2 gets the value of student1
// (student1's field values are copied to
// corresponding field values of student2)
strcpy(student2.name, "Frances Allen"); // change one field value
图 2 显示了执行赋值语句和调用 strcpy 后两个学生变量的值。请注意,该图将 name 字段描述为它们包含的字符串值,而不是 64 个字符的完整数组。
图 2. 执行结构赋值和 strcpy 调用后 student1 和 student2 结构的布局
C 提供了一个 sizeof
运算符,它接受一个类型并返回该类型使用的字节数。sizeof
运算符可用于任何 C 类型,包括结构类型,以查看该类型的变量需要多少内存空间。例如,我们可以打印 struct studentT
类型的大小:
// Note: the `%lu` format placeholder specifies an unsigned long value.
printf("number of bytes in student struct: %lu\n", sizeof(struct studentT));
运行时,此行应打印出至少 76 字节的值,因为 name
数组中有 64 个字符(每个 char
1 字节),int
age
字段有 4 字节,float
gpa
字段有 4 字节,int
grad_yr
字段有 4 字节。在某些机器上,确切的字节数可能大于 76。
下面是一个完整示例程序,定义并演示了 struct studentT
类型的用法:
#include <stdio.h>
#include <string.h>
// Define a new type: struct studentT
// Note that struct definitions should be outside function bodies.
struct studentT {
char name[64];
int age;
float gpa;
int grad_yr;
};
int main(void) {
struct studentT student1, student2;
strcpy(student1.name, "Kwame Salter"); // name field is a char array
student1.age = 18 + 2; // age field is an int
student1.gpa = 3.5; // gpa field is a float
student1.grad_yr = 2020; // grad_yr field is an int
/* Note: printf doesn't have a format placeholder for printing a
* struct studentT (a type we defined). Instead, we'll need to
* individually pass each field to printf. */
printf("name: %s age: %d gpa: %g, year: %d\n",
student1.name, student1.age, student1.gpa, student1.grad_yr);
/* Copy all the field values of student1 into student2. */
student2 = student1;
/* Make a few changes to the student2 variable. */
strcpy(student2.name, "Frances Allen");
student2.grad_yr = student1.grad_yr + 1;
/* Print the fields of student2. */
printf("name: %s age: %d gpa: %g, year: %d\n",
student2.name, student2.age, student2.gpa, student2.grad_yr);
/* Print the size of the struct studentT type. */
printf("number of bytes in student struct: %lu\n", sizeof(struct studentT));
return 0;
}
运行时,该程序输出以下内容:
name: Kwame Salter age: 20 gpa: 3.5, year: 2020
name: Frances Allen age: 20 gpa: 3.5, year: 2021
number of bytes in student struct: 76
结构体是左值(lvalues)
左值 是可以出现在赋值语句左侧的表达式。它是表示内存存储位置的表达式。当我们介绍 C 指针类型以及创建结合 C 数组、结构体和指针的更复杂结构的示例时,重要的是仔细考虑类型并记住哪些 C 表达式是有效的左值(可以在赋值语句的左侧使用)。
从我们目前对 C 的了解来看,基类型的单个变量、数组元素和结构体都是左值。静态声明的数组的名称不是左值(您不能更改内存中静态声明的数组的基地址)。以下示例代码片段根据不同类型的左值状态说明了有效和无效的 C 赋值语句:
struct studentT {
char name[32];
int age;
float gpa;
int grad_yr;
>};
>int main(void) {
struct studentT student1, student2;
int x;
char arr[10], ch;
x = 10; // Valid C: x is an lvalue
ch = 'm'; // Valid C: ch is an lvalue
student1.age = 18; // Valid C: age field is an lvalue
student2 = student1; // Valid C: student2 is an lvalue
arr[3] = ch; // Valid C: arr[3] is an lvalue
x + 1 = 8; // Invalid C: x+1 is not an lvalue
arr = "hello"; // Invalid C: arr is not an lvalue
// cannot change base addr of statically declared array
// (use strcpy to copy the string value "hello" to arr)
student1.name = student2.name; // Invalid C: name field is not an lvalue
// (the base address of a statically
// declared array cannot be changed)
16.6.4. 将结构体传递给函数
在 C 语言中,所有类型的参数都是通过值传递给函数的。因此,如果一个函数有一个结构体类型参数,那么当使用结构体参数调用时,参数的值将传递给其参数,这意味着该参数获得其参数值的副本。结构体变量的值是其内存的内容,这就是为什么我们可以在单个赋值语句中将一个结构的字段赋值给另一个结构体,如下所示:
student2 = student1;
因为结构体变量的值代表其内存的全部内容,所以将结构体作为参数传递给函数会为参数提供该参数结构体所有字段值的副本。如果函数更改了结构体参数的字段值,则对参数字段值的更改不会对参数的相应字段值产生任何影响。也就是说,对参数字段的更改只会修改参数这些字段的内存位置中的值,而不会修改参数这些字段的内存位置中的值。
下面是一个 完整示例程序,其中使用了 checkID 函数,该函数带有一个结构体参数:
#include <stdio.h>
#include <string.h>
/* struct type definition: */
struct studentT {
char name[64];
int age;
float gpa;
int grad_yr;
};
/* function prototype (prototype: a declaration of the
* checkID function so that main can call it, its full
* definition is listed after main function in the file):
*/
int checkID(struct studentT s1, int min_age);
int main(void) {
int can_vote;
struct studentT student;
strcpy(student.name, "Ruth");
student.age = 17;
student.gpa = 3.5;
student.grad_yr = 2021;
can_vote = checkID(student, 18);
if (can_vote) {
printf("%s is %d years old and can vote.\n",
student.name, student.age);
} else {
printf("%s is only %d years old and cannot vote.\n",
student.name, student.age);
}
return 0;
}
/* check if a student is at least the min age
* s: a student
* min_age: a minimum age value to test
* returns: 1 if the student is min_age or older, 0 otherwise
*/
int checkID(struct studentT s, int min_age) {
int ret = 1; // initialize the return value to 1 (true)
if (s.age < min_age) {
ret = 0; // update the return value to 0 (false)
// let's try changing the student's age
s.age = min_age + 1;
}
printf("%s is %d years old\n", s.name, s.age);
return ret;
}
当 main
调用 checkID
时,student
结构体的值(其所有字段的内存内容的副本)被传递给 s
参数。当函数更改其参数的 age
字段的值时,它不会影响其参数(student
)的 age
字段。通过运行程序可以看到此行为,程序输出以下内容:
Ruth is 19 years old
Ruth is only 17 years old and cannot vote.
输出显示,当 checkID
打印 age
字段时,它反映了函数对参数 s
的 age
字段的更改。然而,在函数调用返回后,main
打印的 student
的 age
字段的值与调用 checkID
之前的值相同。图 3 展示了 checkID
函数返回之前调用堆栈的内容。
图 3. checkID 函数返回前的调用堆栈内容
当结构体包含静态声明的数组字段(如 struct studentT
中的 name
字段)时,理解结构体参数的按值传递(pass-by-value)语义尤为重要。当将这样的结构体传递给函数时,结构体参数的整个内存内容(包括数组字段中的每个数组元素)都将复制到其参数中。如果函数更改了参数结构体的数组内容,则这些更改将不会在函数返回后保留。考虑到我们对数组如何传递给函数的了解,这种行为可能看起来很奇怪,但它与前面描述的结构复制行为一致。